package client;

import org.json.java.JSONArray;
import org.json.java.JSONException;
import org.json.java.JSONObject;
import server.ChatServer;

import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created by IntelliJ IDEA.
 * User: ryan
 * Date: 12/1/11
 * Time: 3:41 PM
 * To change this template use File | Settings | File Templates.
 */
public class ClientImpl implements Client,Runnable{
    private Socket s;
    private BufferedWriter out;
    private BufferedReader in;
    private String name;
    private volatile boolean connected;

    //value to indicate if we should close the socket
    //volatile so that each thread reads the same value
    private volatile boolean keepGoing;


    private final List<MessageReceiveListener> messageReceiveListeners;
    private final List<ClientConnectionListener> clientConnectionListeners;

    //these are used for the getAllClients methods
    private volatile List<String> clients;

    private CountDownLatch waitingForAllClientsResponse;

    public ClientImpl(){
        messageReceiveListeners = new LinkedList<MessageReceiveListener>();
        clientConnectionListeners = new LinkedList<ClientConnectionListener>();
        clients = new ArrayList<String>();
        connected =false;
    }



    public boolean connect(String name, String host, int port){
        keepGoing=true;
        try {
            this.name=name;
            s = new Socket(host,port);
            s.setKeepAlive(true);
            out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            in =  new BufferedReader(new InputStreamReader(s.getInputStream()));

            Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" client connected");

            //As soon as we connect we send our name
            out.append(name);
            out.append("\n");
            out.flush();
            connected=true;
            Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" client name sent");
            //Start a thread to listen for incoming messages
            Thread listener = new Thread(this);
            listener.start();
            Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" lister started");

        } catch (IOException e) {
            e.printStackTrace(System.err);
            return false;
        }


        return true;
    }

    public void run() {
        //read in the name
        //receive a message, print it out to all others(including self)
        Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" listening for incoming messages");
        while(keepGoing){
            JSONObject msg = getMessage();
            if(msg!=null){
                //here we fire the various message received events
                try {
                    ChatServer.MESSAGE_TYPE type = ChatServer.MESSAGE_TYPE.valueOf(msg.getString("type"));

                    if(type== ChatServer.MESSAGE_TYPE.MESSAGE){
                        String name = msg.getString("name");
                        String message = msg.getString("message");
                        for(MessageReceiveListener l: messageReceiveListeners){
                            l.onReceive(new MessageEvent(name,message));
                        }
                    }else if(type == ChatServer.MESSAGE_TYPE.CONNECT){
                        String name = msg.getString("name");
                        for(ClientConnectionListener l: clientConnectionListeners){
                            l.onClientConnect(new ClientConnectionEvent(name));
                        }
                    }else if(type == ChatServer.MESSAGE_TYPE.DISCONNECT){
                        String name = msg.getString("name");
                        for(ClientConnectionListener l: clientConnectionListeners){
                            l.onClientDisconnect(new ClientConnectionEvent(name));
                        }
                    }else if(type == ChatServer.MESSAGE_TYPE.CLIENTS){
                        System.out.println("Clients list event received");
                        JSONArray arr = msg.getJSONArray("clients");
                        clients.clear();
                        for(int i= 0;i<arr.length();i++){
                            String name = arr.getString(i);
                            clients.add(name);
                        }
                        waitingForAllClientsResponse.countDown();
                    }
                } catch (JSONException e) {
                    e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                }
            }
            Thread.yield();
        }

    }

    public void sendMessage(String message) {
        JSONObject msg = new JSONObject();
        try{
            msg.put("type", ChatServer.MESSAGE_TYPE.MESSAGE.toString());
            msg.put("name", name);
            msg.put("message", message.replaceAll("[\n\r]+"," "));
            out.append(msg.toString());
            out.append("\n");
            out.flush();
        }catch(JSONException e){
            e.printStackTrace(System.err);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     *
     * @return JSONObject with fields name, type, message
     */
    public JSONObject getMessage() {
        Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" waiting for message");
        String message="";
        try{
            //Read message
            message = in.readLine();
            Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" message received: "+message);
            //return the completed message
        }catch(IOException e){
            //e.printStackTrace(System.err);
        }
        JSONObject msg = null;
        try {
            msg = new JSONObject(message);
        } catch (JSONException e) {
            //e.printStackTrace(System.err);
        }
        return msg;
    }

    public void disconnect() {
        Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" disconnecting");
        try {
            this.s.close();
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
        keepGoing=false;
    }

    public void addMessageReceivedListener(MessageReceiveListener l) {
        messageReceiveListeners.add(l);
    }

    public void addClientConnectionListener(ClientConnectionListener l) {
        clientConnectionListeners.add(l);
    }

    /**
     * NOTE:  This method is not thread safe
     * @return list of all the clients connected to the server
     */
    public List<String> getAllClients() {
        try{
            //initialize a CountDownLatch.
            waitingForAllClientsResponse = new CountDownLatch(1);
             Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" requesting client list");

            //send a message requesting clients to the server
            JSONObject msg = new JSONObject();
            msg.put("type",ChatServer.MESSAGE_TYPE.CLIENTS.toString());
            out.append(msg.toString());
            out.append("\n");
            out.flush();

            //Block until the latch is decremented by the message receiver
            Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" waiting for client list");
            waitingForAllClientsResponse.await();

        }catch(InterruptedException e){
             e.printStackTrace(System.err);
        }catch(JSONException e){
                e.printStackTrace(System.err);
        }catch(IOException e){
            e.printStackTrace(System.err);
        }
        //at this point clients has been populated by the message receiver thread
        Logger.getLogger(this.getClass().getName()).log(Level.INFO,name+" clients list returning");
        return clients;
    }
}
